~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
1059 lines · 31.8 KB Raw History
package clusterprovider

import (
	"cmp"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"reflect"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/apiserverfront"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/openbaoctl"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/pgunit"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/postgresctl"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/procgroup"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/vault/openbao"
)

const (
	LabelCluster     = "socialweb.computer/cluster"
	LabelMember      = "socialweb.computer/member"
	LabelVerified    = "socialweb.computer/verified-invocation"
	MemberFinalizer  = "socialweb.computer/cluster-member"
	ControlPlaneFile = "controlplane.json"
	ActiveSlot       = "a"

	DefaultPlaceTimeout = time.Minute
	DefaultStoreTimeout = time.Minute
	DefaultReapTimeout  = time.Minute
	MemberOutputCap     = 64 << 20
	memberStopGrace     = 30
	FrontFDStore        = 32
	frontStopGrace      = 10
	memberOutputTail    = 1 << 20
	memberRemoveTimeout = 30 * time.Second
)

var ErrMemberLost = errors.New("clusterprovider: a member unit was lost while it was being placed")

type UnitsConfig struct {
	HostProcesses Objects[v1alpha1.HostProcess]

	Postgres Objects[v1alpha1.Postgres]

	OpenBaoServers Objects[v1alpha1.OpenBaoServer]

	Prefix string

	StateDir string

	VaultStateDir string

	Resolve func(name string) (string, error)

	PlaceTimeout time.Duration

	StoreTimeout time.Duration

	ReapTimeout time.Duration

	Logins *openbao.Logins

	Log *slog.Logger
}

type UnitsHost struct {
	c   UnitsConfig
	hps Objects[v1alpha1.HostProcess]
	pgs Objects[v1alpha1.Postgres]
	obs Objects[v1alpha1.OpenBaoServer]

	mu     sync.Mutex
	stores map[string]Store
	keys   map[string]*sync.Mutex
}

type Store struct {
	Object    string
	Unit      string
	SocketDir string
	User      string
}

func (s Store) DSN() string { return pgunit.DSN(s.SocketDir, s.User, PostgresDatabase) }

func (s Store) VaultDSN() string { return pgunit.DSN(s.SocketDir, s.User, PostgresVaultDatabase) }

func (s Store) HasBootstrapRow(ctx context.Context) (bool, error) {
	rows, err := pgunit.Query(ctx, s.SocketDir, s.User, PostgresDatabase,
		"select count(*) from kine where name like '/bootstrap/%'")
	if err != nil {
		return false, fmt.Errorf("clusterprovider: reading k3s's bootstrap row out of %s: %w", s.Object, err)
	}
	if len(rows) == 0 || len(rows[0]) == 0 {
		return false, fmt.Errorf("clusterprovider: %s answered no row count", s.Object)
	}
	n, err := strconv.Atoi(rows[0][0])
	if err != nil {
		return false, err
	}
	return n > 0, nil
}

func (s Store) DeleteBootstrapRows(ctx context.Context) error {
	_, err := pgunit.Query(ctx, s.SocketDir, s.User, PostgresDatabase, "delete from kine where name like '/bootstrap/%'")
	return err
}

type ControlPlane struct {
	ActiveSlot string                `json:"activeSlot"`
	Slice      string                `json:"slice"`
	Store      string                `json:"store"`
	Members    []ControlPlaneMember  `json:"members"`
	Front      *ControlPlaneFront    `json:"front,omitempty"`
	Slots      map[string]SlotRecord `json:"slots,omitempty"`
	Links      map[string]LinkRecord `json:"links,omitempty"`
	LastRoll   string                `json:"lastRoll,omitempty"`
	WrittenAt  string                `json:"writtenAt"`
}

type SlotRecord struct {
	K3s        string `json:"k3s"`
	Kine       string `json:"kine"`
	K3sVersion string `json:"k3sVersion,omitempty"`
	Port       int    `json:"port"`
	KinePort   int    `json:"kinePort"`
	DataDir    string `json:"dataDir"`
	Hostname   string `json:"hostname,omitempty"`
	K3sUnit    string `json:"k3sUnit"`
	KineUnit   string `json:"kineUnit"`
	Invocation string `json:"invocation,omitempty"`
	StartedAt  string `json:"startedAt,omitempty"`
}

type LinkRecord struct {
	Slot     string `json:"slot"`
	Dumbpipe string `json:"dumbpipe,omitempty"`
	Port     int    `json:"port,omitempty"`
}

func (h *UnitsHost) lock(key string) func() {
	h.mu.Lock()
	l, ok := h.keys[key]
	if !ok {
		l = &sync.Mutex{}
		h.keys[key] = l
	}
	h.mu.Unlock()
	l.Lock()
	return l.Unlock
}

func (h *UnitsHost) SlotState(key string) SlotState {
	cp, err := h.readControlPlane(key)
	if err != nil || cp.ActiveSlot == "" {
		return SlotState{Slot: ActiveSlot}
	}
	r := cp.Slots[cp.ActiveSlot]
	return SlotState{Slot: cp.ActiveSlot, K3s: r.K3s, Kine: r.Kine}
}

func (h *UnitsHost) ControlPlane(key string) (ControlPlane, error) { return h.readControlPlane(key) }

type ControlPlaneFront struct {
	Address    string `json:"address"`
	Backend    string `json:"backend"`
	Generation int64  `json:"generation"`
	Control    string `json:"control"`
}

type ControlPlaneMember struct {
	Name         string `json:"name"`
	Unit         string `json:"unit"`
	Object       string `json:"object"`
	InvocationID string `json:"invocationID"`
}

func NewUnitsHost(c UnitsConfig) (*UnitsHost, error) {
	if c.HostProcesses == nil || c.Postgres == nil || c.OpenBaoServers == nil {
		return nil, errors.New("clusterprovider: the units host needs the kcp the HostProcess supervisor watches")
	}
	if c.Prefix == "" {
		return nil, errors.New("clusterprovider: the units host needs the unit prefix its supervisor places under")
	}
	if c.StateDir == "" {
		return nil, errors.New("clusterprovider: the units host needs the state dir the cluster data dirs live in")
	}
	if c.Resolve == nil {
		c.Resolve = exec.LookPath
	}
	c.PlaceTimeout = cmp.Or(c.PlaceTimeout, DefaultPlaceTimeout)
	c.StoreTimeout = cmp.Or(c.StoreTimeout, DefaultStoreTimeout)
	c.ReapTimeout = cmp.Or(c.ReapTimeout, DefaultReapTimeout)
	if c.Log == nil {
		c.Log = slog.Default()
	}
	return &UnitsHost{c: c, hps: c.HostProcesses, pgs: c.Postgres, obs: c.OpenBaoServers,
		stores: map[string]Store{}, keys: map[string]*sync.Mutex{}}, nil
}

func slotOr(slot string) string {
	if slot == "" {
		return ActiveSlot
	}
	return slot
}

func StepMember(st procgroup.Step) string { return SlotMember(st.Name, st.Slot) }

func SlotMember(c procgroup.Component, slot string) string {
	switch c {
	case procgroup.ComponentKine:
		return "kine@" + slotOr(slot)
	case procgroup.ComponentK3s:
		return "k3s@" + slotOr(slot)
	case procgroup.ComponentApiserverLink, procgroup.ComponentOpenBaoLink:
		if slot != "" && slot != ActiveSlot {
			return MemberName(c) + "@" + slot
		}
	}
	return MemberName(c)
}

func MemberName(c procgroup.Component) string {
	switch c {
	case procgroup.ComponentKine:
		return "kine@" + ActiveSlot
	case procgroup.ComponentK3s:
		return "k3s@" + ActiveSlot
	case procgroup.ComponentApiserverFront:
		return "front"
	case procgroup.ComponentOpenBao:
		return "bao"
	case procgroup.ComponentApiserverLink:
		return "link-apiserver"
	case procgroup.ComponentOpenBaoLink:
		return "link-openbao"
	case procgroup.ComponentCloudControllerManager:
		return "ccm"
	case procgroup.ComponentExternalSecrets:
		return "eso"
	case procgroup.ComponentExternalSecretsWebhook:
		return "esowh"
	}
	return string(c)
}

func (h *UnitsHost) Unit(key, member string) string {
	return h.c.Prefix + "c-" + key + "-" + member + ".service"
}

func memberLabel(member string) string { return strings.ReplaceAll(member, "@", ".") }

func labelMember(label string) string { return strings.Replace(label, ".", "@", 1) }

func ObjectName(key, member string) string {
	return "c-" + key + "-" + strings.ReplaceAll(member, "@", "-")
}

func (h *UnitsHost) Slice(key string) string {
	return h.c.Prefix + "cluster-" + strings.ReplaceAll(key, "-", "_") + ".slice"
}

func StoreName(key string) string {
	if len(key) <= 32 {
		return key
	}
	sum := sha256.Sum256([]byte(key))
	return strings.TrimRight(key[:23], "-") + "-" + hex.EncodeToString(sum[:])[:8]
}

func (h *UnitsHost) StoreUnit(key string) string {
	return h.c.Prefix + postgresctl.ChildPrefix + StoreName(key) + ".service"
}

func (h *UnitsHost) StartStore(ctx context.Context, key string) error {
	name := StoreName(key)
	cur, err := h.pgs.Get(ctx, name)
	if apierrors.IsNotFound(err) {
		o := v1alpha1.Postgres{ObjectMeta: metav1.ObjectMeta{Name: name, Labels: map[string]string{LabelCluster: key}},
			Spec: v1alpha1.PostgresSpec{Databases: []string{PostgresDatabase, PostgresVaultDatabase}}}
		if err := h.pgs.Create(ctx, &o); err != nil && !apierrors.IsAlreadyExists(err) {
			return fmt.Errorf("clusterprovider: creating the Postgres object %s of %s: %w", name, key, err)
		}
		h.c.Log.Info("cluster provider: the cluster's store is a Postgres object", "cluster", key, "postgres", name,
			"unit", h.StoreUnit(key))
	} else if err != nil {
		return err
	} else if cur.DeletionTimestamp != nil {
		return fmt.Errorf("clusterprovider: the Postgres object %s of %s is being deleted", name, key)
	}
	if _, cached := h.Store(key); !cached {
		if o, err := h.pgs.Get(ctx, name); err == nil {
			if o.Status.Phase == v1alpha1.PostgresPhaseReady && o.Status.SocketDir != "" {
				h.mu.Lock()
				h.stores[key] = Store{Object: name, Unit: h.StoreUnit(key), SocketDir: o.Status.SocketDir, User: o.Status.User}
				h.mu.Unlock()
			}
		}
	}
	return nil
}

func (h *UnitsHost) EnsureStore(ctx context.Context, key string) (Store, error) {
	name := StoreName(key)
	if err := h.StartStore(ctx, key); err != nil {
		return Store{}, err
	}
	unavailable := func(o *v1alpha1.Postgres) error {
		return fmt.Errorf("%w: the Postgres object %s of %s is %s: %s", procgroup.ErrPostgresUnavailable,
			name, key, o.Status.Phase, o.Status.Message)
	}
	o, err := h.pgs.Await(ctx, name, h.c.StoreTimeout, func(o *v1alpha1.Postgres) (bool, error) {
		if o == nil {
			return false, nil
		}
		if o.Status.Phase == v1alpha1.PostgresPhaseReady && o.Status.SocketDir != "" {
			return true, nil
		}
		if o.Status.Phase == v1alpha1.PostgresPhaseFailed && o.Status.ObservedGeneration == o.Generation {
			return false, unavailable(o)
		}
		return false, nil
	})
	if err != nil {
		if timedOut(err) && o != nil {
			return Store{}, unavailable(o)
		}
		return Store{}, err
	}
	s := Store{Object: name, Unit: h.StoreUnit(key), SocketDir: o.Status.SocketDir, User: o.Status.User}
	h.mu.Lock()
	h.stores[key] = s
	h.mu.Unlock()
	return s, nil
}

func (h *UnitsHost) Store(key string) (Store, bool) {
	h.mu.Lock()
	defer h.mu.Unlock()
	s, ok := h.stores[key]
	return s, ok
}

func (h *UnitsHost) storeReady(ctx context.Context, key string) bool {
	o, err := h.pgs.Get(ctx, StoreName(key))
	return err == nil && o.Status.Phase == v1alpha1.PostgresPhaseReady
}

func (h *UnitsHost) DeleteStore(ctx context.Context, key string) error {
	h.mu.Lock()
	delete(h.stores, key)
	h.mu.Unlock()
	name := StoreName(key)
	cur, err := h.pgs.Get(ctx, name)
	if apierrors.IsNotFound(err) {
		return nil
	}
	if err != nil {
		return err
	}
	cur.Spec.DeleteData = true
	if err := h.pgs.Update(ctx, cur); err != nil && !apierrors.IsNotFound(err) {
		return err
	}
	err = h.pgs.Delete(ctx, name)
	if err != nil && !apierrors.IsNotFound(err) {
		return err
	}
	return nil
}

func (h *UnitsHost) StoreRemoval(ctx context.Context, key string) (bool, string, error) {
	o, err := h.pgs.Get(ctx, StoreName(key))
	if apierrors.IsNotFound(err) {
		return true, "", nil
	}
	if err != nil {
		return false, "", err
	}
	return false, fmt.Sprintf("the Postgres object %s is %s: %s", StoreName(key), o.Status.Phase, o.Status.Message), nil
}

func (h *UnitsHost) PurgeData(key string) error {
	dir, err := ClusterDataDir(h.c.StateDir, key)
	if err != nil {
		return err
	}
	dirs := []string{dir}
	if key != "" && filepath.Base(key) == key && key != "." && key != ".." {
		dirs = append(dirs, filepath.Join(h.c.StateDir, key))
	}
	if h.c.VaultStateDir != "" {
		dirs = append(dirs, openbaoctl.ServerDir(h.c.VaultStateDir, h.VaultServerName(key)))
	}
	for _, d := range dirs {
		if _, err := os.Lstat(d); errors.Is(err, os.ErrNotExist) {
			continue
		}
		if err := os.RemoveAll(d); err != nil {
			return fmt.Errorf("clusterprovider: purging %s of deleted cluster %s: %w", d, key, err)
		}
		h.c.Log.Info("cluster provider: identity bundle purged", "cluster", key, "dir", d)
	}
	return nil
}

func (h *UnitsHost) Members(ctx context.Context, key string) ([]v1alpha1.HostProcess, error) {
	return h.hps.List(ctx, LabelCluster+"="+key)
}

func (h *UnitsHost) gateAlive(ctx context.Context, key, member, inv string, gate func(context.Context) error) error {
	var wg sync.WaitGroup
	defer wg.Wait()
	gctx, cancel := context.WithCancelCause(ctx)
	defer cancel(nil)
	wg.Go(func() {
		_, _ = h.hps.Await(gctx, ObjectName(key, member), 0, func(o *v1alpha1.HostProcess) (bool, error) {
			if dead := memberDead(o, inv); dead != "" {
				cancel(fmt.Errorf("clusterprovider: %s of %s %s", member, key, dead))
				return true, nil
			}
			return false, nil
		})
	})
	err := gate(gctx)
	if cause := context.Cause(gctx); err != nil && cause != nil && !errors.Is(cause, context.Canceled) && !errors.Is(cause, context.DeadlineExceeded) {
		return fmt.Errorf("%w; the gate saw: %v", cause, err)
	}
	return err
}

func memberDead(o *v1alpha1.HostProcess, inv string) string {
	if o == nil {
		return "is gone"
	}
	switch o.Status.Phase {
	case v1alpha1.HostProcessPhaseFailed, v1alpha1.HostProcessPhaseLost, v1alpha1.HostProcessPhaseStopped:
		return fmt.Sprintf("is %s %s during its readiness gate: %s", o.Status.Phase, o.Status.LostReason, o.Status.Message)
	}
	if rt := o.Status.Runtime; rt != nil && inv != "" && rt.InvocationID != "" && rt.InvocationID != inv {
		return fmt.Sprintf("restarted during its readiness gate (invocation %s, now %s): %s", inv, rt.InvocationID, o.Status.Message)
	}
	return ""
}

func (h *UnitsHost) member(ctx context.Context, key, member string) (*v1alpha1.HostProcess, error) {
	o, err := h.hps.Get(ctx, ObjectName(key, member))
	if apierrors.IsNotFound(err) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return o, nil
}

func Verified(o *v1alpha1.HostProcess) bool {
	if o == nil || o.DeletionTimestamp != nil || o.Status.Phase != v1alpha1.HostProcessPhaseRunning || o.Status.Runtime == nil {
		return false
	}
	inv := o.Status.Runtime.InvocationID
	return inv != "" && o.Labels[LabelVerified] == inv
}

func (h *UnitsHost) Live(ctx context.Context, key string) bool {
	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()
	m, err := h.Members(ctx, key)
	return err == nil && len(m) > 0
}

func (h *UnitsHost) Unhealthy(ctx context.Context, key string) ([]string, error) {
	members, err := h.Members(ctx, key)
	if err != nil {
		return nil, err
	}
	have := map[string]*v1alpha1.HostProcess{}
	for i := range members {
		have[labelMember(members[i].Labels[LabelMember])] = &members[i]
	}
	var sick []string
	cp, err := h.readControlPlane(key)
	if err != nil && len(members) > 0 {
		sick = append(sick, ControlPlaneFile+": "+err.Error())
	}
	vault := MemberName(procgroup.ComponentOpenBao)
	recorded := map[string]bool{}
	for _, m := range cp.Members {
		recorded[m.Name] = true
		if m.Name == vault {
			if why := h.vaultSick(ctx, key); why != "" {
				sick = append(sick, m.Name+": "+why)
			}
			continue
		}
		if have[m.Name] == nil {
			sick = append(sick, m.Name+": no HostProcess")
		}
	}
	for _, o := range members {
		name := labelMember(o.Labels[LabelMember])
		if len(recorded) > 0 && !recorded[name] {
			continue
		}
		switch {
		case o.DeletionTimestamp != nil:
			sick = append(sick, name+": stopping")
		case o.Status.Phase != v1alpha1.HostProcessPhaseRunning:
			sick = append(sick, strings.TrimSpace(fmt.Sprintf("%s: %s %s %s", name, o.Status.Phase, o.Status.LostReason, o.Status.Message)))
		case !Verified(&o):
			sick = append(sick, fmt.Sprintf("%s: invocation %s is not verified since its restart", name, o.Status.Runtime.InvocationID))
		}
	}
	slices.Sort(sick)
	return sick, nil
}

func (h *UnitsHost) Adoption(ctx context.Context, key string) ([]string, error) {
	members, err := h.Members(ctx, key)
	if err != nil {
		return nil, err
	}
	out := make([]string, 0, len(members))
	for _, o := range members {
		inv := ""
		if o.Status.Runtime != nil {
			inv = o.Status.Runtime.InvocationID
		}
		out = append(out, fmt.Sprintf("%s=%s invocation=%s verified=%t", labelMember(o.Labels[LabelMember]), o.Status.Phase, inv, Verified(&o)))
	}
	slices.Sort(out)
	return out, nil
}

func memberDeps(h *UnitsHost, key string, c procgroup.Component, slot string) []string {
	switch c {
	case procgroup.ComponentApiserverFront:
		return nil
	case procgroup.ComponentKine, procgroup.ComponentOpenBao:
		return []string{h.StoreUnit(key)}
	case procgroup.ComponentK3s:
		return []string{h.Unit(key, SlotMember(procgroup.ComponentKine, slot))}
	case procgroup.ComponentOpenBaoLink:
		return []string{h.Unit(key, MemberName(procgroup.ComponentOpenBao))}
	}
	return []string{h.Unit(key, MemberName(procgroup.ComponentApiserverFront))}
}

func (h *UnitsHost) memberSpec(key string, st procgroup.Step, spec procgroup.Spec) (v1alpha1.HostProcessSpec, error) {
	c := st.Name
	if len(spec.Argv) == 0 {
		return v1alpha1.HostProcessSpec{}, fmt.Errorf("clusterprovider: %s of %s has no argv", c, key)
	}
	exe := spec.Argv[0]
	if !filepath.IsAbs(exe) {
		p, err := h.c.Resolve(exe)
		if err != nil {
			return v1alpha1.HostProcessSpec{}, fmt.Errorf("clusterprovider: %s of %s runs %s, which is not on this host: %w", c, key, exe, err)
		}
		if exe, err = filepath.Abs(p); err != nil {
			return v1alpha1.HostProcessSpec{}, err
		}
	}
	deps := memberDeps(h, key, c, st.Slot)
	out := v1alpha1.HostProcessSpec{Unit: h.Unit(key, StepMember(st)), Exec: exe, Argv: append([]string(nil), spec.Argv[1:]...),
		EnvFiles: append([]string(nil), spec.EnvFiles...), RestartPolicy: v1alpha1.HostProcessRestartOnFailure,
		Stop:  &v1alpha1.HostProcessStop{Signal: "SIGTERM", GraceSeconds: memberStopGrace},
		After: deps, Wants: deps, Slice: h.Slice(key), Placement: v1alpha1.HostProcessPlacementSupervisor}
	switch c {
	case procgroup.ComponentApiserverLink, procgroup.ComponentOpenBaoLink:
		out.RestartPolicy = v1alpha1.HostProcessRestartAlways
	case procgroup.ComponentApiserverFront:
		out.RestartPolicy = v1alpha1.HostProcessRestartAlways
		out.FdStore = FrontFDStore
		out.Stop.GraceSeconds = frontStopGrace
	}
	for _, kv := range spec.Env {
		k, v, _ := strings.Cut(kv, "=")
		out.Env = append(out.Env, v1alpha1.HostProcessEnv{Name: k, Value: v})
	}
	if spec.Log != "" {
		out.Output = &v1alpha1.HostProcessOutput{Stdout: spec.Log, Stderr: spec.Log, CapBytes: MemberOutputCap}
	}
	return out, nil
}

func (h *UnitsHost) remove(ctx context.Context, name string) error {
	for range 5 {
		u, err := h.hps.Get(ctx, name)
		if apierrors.IsNotFound(err) {
			return nil
		}
		if err != nil {
			return err
		}
		fin := slices.DeleteFunc(slices.Clone(u.Finalizers), func(f string) bool { return f == MemberFinalizer })
		if len(fin) != len(u.Finalizers) {
			u.Finalizers = fin
			if err := h.hps.Update(ctx, u); err != nil {
				if apierrors.IsConflict(err) {
					continue
				}
				return err
			}
		}
		if u.DeletionTimestamp == nil {
			if err := h.hps.Delete(ctx, name); err != nil && !apierrors.IsNotFound(err) {
				return err
			}
		}
	}
	if _, err := h.hps.Await(ctx, name, memberRemoveTimeout, absent[v1alpha1.HostProcess]); err != nil {
		if timedOut(err) {
			return fmt.Errorf("clusterprovider: the HostProcess %s was not removed", name)
		}
		return err
	}
	return nil
}

func rotateLog(path string) {
	if path == "" {
		return
	}
	if _, err := os.Stat(path); err == nil {
		os.Rename(path, path+".prev")
	}
}

func (h *UnitsHost) place(ctx context.Context, key, member string, cur *v1alpha1.HostProcess, want v1alpha1.HostProcessSpec) (*v1alpha1.HostProcess, error) {
	name := ObjectName(key, member)
	if cur != nil {
		switch {
		case cur.DeletionTimestamp != nil, cur.Status.Phase == v1alpha1.HostProcessPhaseLost, cur.Status.Phase == v1alpha1.HostProcessPhaseStopped:
			h.c.Log.Warn("cluster provider: a member is not running; it is placed again", "cluster", key, "member", member,
				"phase", cur.Status.Phase, "lostReason", cur.Status.LostReason, "message", cur.Status.Message)
			if err := h.remove(ctx, name); err != nil {
				return nil, err
			}
			cur = nil
		case !reflect.DeepEqual(cur.Spec, want):
			cur.Spec = want
			if err := h.hps.Update(ctx, cur); err != nil {
				return nil, err
			}
			h.c.Log.Info("cluster provider: a member's spec changed; the running unit keeps the old one until it restarts",
				"cluster", key, "member", member)
		}
	}
	if cur == nil {
		if want.Output != nil {
			rotateLog(want.Output.Stdout)
		}
		o := v1alpha1.HostProcess{ObjectMeta: metav1.ObjectMeta{Name: name,
			Labels:     map[string]string{LabelCluster: key, LabelMember: memberLabel(member)},
			Finalizers: []string{MemberFinalizer}}, Spec: want}
		if err := h.hps.Create(ctx, &o); err != nil {
			return nil, fmt.Errorf("clusterprovider: creating the HostProcess of %s of %s: %w", member, key, err)
		}
		h.c.Log.Info("cluster provider: member placed", "cluster", key, "member", member, "unit", want.Unit, "slice", want.Slice,
			"after", want.After)
	}
	o, err := h.hps.Await(ctx, name, h.c.PlaceTimeout, func(o *v1alpha1.HostProcess) (bool, error) {
		if o == nil {
			return false, nil
		}
		switch o.Status.Phase {
		case v1alpha1.HostProcessPhaseRunning:
			return o.Status.Runtime != nil && o.Status.Runtime.InvocationID != "", nil
		case v1alpha1.HostProcessPhaseLost:
			return false, fmt.Errorf("%w: %s of %s is Lost %s: %s", ErrMemberLost, member, key, o.Status.LostReason, o.Status.Message)
		case v1alpha1.HostProcessPhaseFailed:
			return false, fmt.Errorf("clusterprovider: %s of %s is Failed: %s", member, key, o.Status.Message)
		}
		return false, nil
	})
	switch {
	case err == nil:
		return o, nil
	case !timedOut(err):
		return nil, err
	case o == nil:
		return nil, fmt.Errorf("clusterprovider: the HostProcess of %s of %s vanished while it was placed", member, key)
	}
	return nil, fmt.Errorf("clusterprovider: %s of %s is %s after %s: %s", member, key, o.Status.Phase, h.c.PlaceTimeout, o.Status.Message)
}

func (h *UnitsHost) verify(ctx context.Context, key, member, inv string) error {
	for range 5 {
		u, err := h.hps.Get(ctx, ObjectName(key, member))
		if err != nil {
			return err
		}
		if u.Labels == nil {
			u.Labels = map[string]string{}
		}
		u.Labels[LabelVerified] = inv
		if err = h.hps.Update(ctx, u); err == nil || !apierrors.IsConflict(err) {
			return err
		}
	}
	return fmt.Errorf("clusterprovider: recording the verified invocation of %s of %s kept conflicting", member, key)
}

type unitMember struct{ path string }

func (m unitMember) Output() string {
	f, err := os.Open(m.path)
	if err != nil {
		return ""
	}
	defer f.Close()
	if st, err := f.Stat(); err == nil && st.Size() > memberOutputTail {
		f.Seek(st.Size()-memberOutputTail, io.SeekStart)
	}
	b, _ := io.ReadAll(f)
	return string(b)
}

func (h *UnitsHost) Spawn(ctx context.Context, key string, steps []procgroup.Step) error {
	store, ok := h.Store(key)
	if !ok {
		var err error
		if store, err = h.EnsureStore(ctx, key); err != nil {
			return err
		}
	}
	unlock := h.lock(key)
	defer unlock()
	prev, _ := h.readControlPlane(key)
	cp := ControlPlane{ActiveSlot: ActiveSlot, Slice: h.Slice(key), Store: store.Unit, Front: prev.Front, Slots: prev.Slots,
		Links: prev.Links, LastRoll: prev.LastRoll}
	for _, st := range steps {
		member := StepMember(st)
		if st.Name == procgroup.ComponentK3s {
			cp.ActiveSlot = slotOr(st.Slot)
		}
		cp.Members = append(cp.Members, ControlPlaneMember{Name: member, Unit: h.Unit(key, member), Object: ObjectName(key, member)})
	}
	if err := h.writeControlPlane(key, cp); err != nil {
		return err
	}
	var cpMu sync.Mutex
	run := func(ctx context.Context, i int, st procgroup.Step) error {
		if (st.Name == procgroup.ComponentKine || st.Name == procgroup.ComponentOpenBao) && !h.storeReady(ctx, key) {
			return fmt.Errorf("%w (cluster %s, Postgres %s)", procgroup.ErrPostgresUnavailable, key, store.Object)
		}
		if st.Name == procgroup.ComponentOpenBao {
			o, err := h.placeVault(ctx, key, st)
			if err != nil {
				return fmt.Errorf("clusterprovider: the vault of %s: %w", key, err)
			}
			if st.Ready != nil {
				if err := st.Ready(ctx, unitMember{path: st.Spec.Log}); err != nil {
					return fmt.Errorf("clusterprovider: the vault of %s never became ready: %w", key, err)
				}
			}
			cpMu.Lock()
			if o.Status.Runtime != nil {
				cp.Members[i].InvocationID = o.Status.Runtime.Instance
			}
			cp.Members[i].Object = o.Name
			cpMu.Unlock()
			h.c.Log.Info("cluster provider: vault ready and synced to its declaration", "cluster", key, "openbaoserver", o.Name,
				"unit", o.Spec.Unit, "address", o.Status.Address)
			return nil
		}
		inv, err := h.runStep(ctx, key, st)
		if err != nil {
			return err
		}
		cpMu.Lock()
		cp.Members[i].InvocationID = inv
		cpMu.Unlock()
		return nil
	}
	if err := runGraph(ctx, steps, run); err != nil {
		return err
	}
	if f, err := h.frontRecord(ctx, key); err == nil {
		cp.Front = f
	}
	return h.writeControlPlane(key, cp)
}

func stepNeeds(steps []procgroup.Step, i int) []int {
	index := map[procgroup.Component]int{}
	for j, st := range steps {
		if _, seen := index[st.Name]; !seen {
			index[st.Name] = j
		}
	}
	var names []procgroup.Component
	switch steps[i].Name {
	case procgroup.ComponentApiserverFront:
	case procgroup.ComponentOpenBao, procgroup.ComponentKine:
		names = []procgroup.Component{procgroup.ComponentApiserverFront}
	case procgroup.ComponentK3s:
		names = []procgroup.Component{procgroup.ComponentApiserverFront, procgroup.ComponentKine, procgroup.ComponentOpenBao}
	case procgroup.ComponentCloudControllerManager, procgroup.ComponentExternalSecretsWebhook:
		names = []procgroup.Component{procgroup.ComponentK3s}
	case procgroup.ComponentExternalSecrets:
		names = []procgroup.Component{procgroup.ComponentK3s, procgroup.ComponentOpenBao}
	case procgroup.ComponentApiserverLink:
		names = []procgroup.Component{procgroup.ComponentApiserverFront, procgroup.ComponentK3s}
	case procgroup.ComponentOpenBaoLink:
		names = []procgroup.Component{procgroup.ComponentOpenBao}
	default:
		out := make([]int, 0, i)
		for j := range i {
			out = append(out, j)
		}
		return out
	}
	out := make([]int, 0, len(names))
	for _, n := range names {
		if j, ok := index[n]; ok && j != i {
			out = append(out, j)
		}
	}
	return out
}

func runGraph(ctx context.Context, steps []procgroup.Step, run func(context.Context, int, procgroup.Step) error) error {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	done := make([]chan struct{}, len(steps))
	for i := range done {
		done[i] = make(chan struct{})
	}
	var (
		wg       sync.WaitGroup
		errOnce  sync.Once
		firstErr error
	)
	fail := func(err error) {
		errOnce.Do(func() {
			firstErr = err
			cancel()
		})
	}
	for i, st := range steps {
		wg.Go(func() {
			for _, j := range stepNeeds(steps, i) {
				select {
				case <-done[j]:
				case <-ctx.Done():
					return
				}
			}
			if err := run(ctx, i, st); err != nil {
				fail(err)
				return
			}
			close(done[i])
		})
	}
	wg.Wait()
	if firstErr != nil {
		return firstErr
	}
	return ctx.Err()
}

func (h *UnitsHost) runStep(ctx context.Context, key string, st procgroup.Step) (string, error) {
	member := StepMember(st)
	cur, err := h.member(ctx, key, member)
	if err != nil {
		return "", err
	}
	if Verified(cur) {
		h.c.Log.Info("cluster provider: member adopted", "cluster", key, "member", member, "unit", cur.Spec.Unit,
			"invocation", cur.Status.Runtime.InvocationID)
		return cur.Status.Runtime.InvocationID, nil
	}
	spec := st.Spec
	if st.Prepare != nil {
		if spec, err = st.Prepare(ctx, spec); err != nil {
			return "", fmt.Errorf("clusterprovider: preparing %s of %s: %w", member, key, err)
		}
	}
	want, err := h.memberSpec(key, st, spec)
	if err != nil {
		return "", err
	}
	o, err := h.place(ctx, key, member, cur, want)
	if err != nil {
		return "", err
	}
	if st.Ready == nil {
		return "", fmt.Errorf("clusterprovider: %s of %s has no readiness gate", member, key)
	}
	if err := h.gateAlive(ctx, key, member, o.Status.Runtime.InvocationID, func(ctx context.Context) error {
		return st.Ready(ctx, unitMember{path: spec.Log})
	}); err != nil {
		return "", fmt.Errorf("clusterprovider: %s of %s never became ready: %w", member, key, err)
	}
	inv := o.Status.Runtime.InvocationID
	if err := h.verify(ctx, key, member, inv); err != nil {
		return "", err
	}
	h.c.Log.Info("cluster provider: member ready", "cluster", key, "member", member, "unit", o.Spec.Unit, "invocation", inv)
	return inv, nil
}

func (h *UnitsHost) EnsureFront(ctx context.Context, key string, st procgroup.Step) error {
	if st.Name != procgroup.ComponentApiserverFront {
		return fmt.Errorf("clusterprovider: EnsureFront was handed %s, not the apiserver front", st.Name)
	}
	unlock := h.lock(key)
	defer unlock()
	_, err := h.runStep(ctx, key, st)
	return err
}

func (h *UnitsHost) FrontControl(key string) (string, error) {
	dir, err := ClusterDataDir(h.c.StateDir, key)
	if err != nil {
		return "", err
	}
	return FrontControlPath(h.c.StateDir, dir, key), nil
}

func (h *UnitsHost) frontRecord(ctx context.Context, key string) (*ControlPlaneFront, error) {
	ctl, err := h.FrontControl(key)
	if err != nil {
		return nil, err
	}
	st, err := (apiserverfront.Client{Path: ctl}).Status(ctx)
	if err != nil {
		return nil, err
	}
	return &ControlPlaneFront{Address: st.Address, Backend: st.Backend, Generation: st.Generation, Control: ctl}, nil
}

func (h *UnitsHost) controlPlanePath(key string) (string, error) {
	dir, err := ClusterDataDir(h.c.StateDir, key)
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, ControlPlaneFile), nil
}

func (h *UnitsHost) readControlPlane(key string) (ControlPlane, error) {
	var cp ControlPlane
	p, err := h.controlPlanePath(key)
	if err != nil {
		return cp, err
	}
	b, err := os.ReadFile(p)
	if err != nil {
		return cp, err
	}
	return cp, json.Unmarshal(b, &cp)
}

func (h *UnitsHost) writeControlPlane(key string, cp ControlPlane) error {
	p, err := h.controlPlanePath(key)
	if err != nil {
		return err
	}
	if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil {
		return err
	}
	cp.WrittenAt = time.Now().UTC().Format(time.RFC3339Nano)
	b, err := json.MarshalIndent(cp, "", "  ")
	if err != nil {
		return err
	}
	tmp := p + ".tmp"
	if err := os.WriteFile(tmp, b, 0o600); err != nil {
		return err
	}
	return os.Rename(tmp, p)
}

func (h *UnitsHost) Reap(ctx context.Context, key string) error {
	unlock := h.lock(key)
	defer unlock()
	members, err := h.Members(ctx, key)
	if err != nil {
		return err
	}
	for _, o := range members {
		if o.DeletionTimestamp != nil {
			continue
		}
		if err := h.hps.Delete(ctx, o.Name); err != nil && !apierrors.IsNotFound(err) {
			return err
		}
	}
	if err := h.reapVault(ctx, key); err != nil {
		return err
	}
	if len(members) == 0 {
		return nil
	}
	h.c.Log.Info("cluster provider: the cluster's slice is stopped; waiting for every member to be inactive",
		"cluster", key, "slice", h.Slice(key), "members", len(members))
	pending := 0
	removed := map[string]bool{}
	err = h.hps.AwaitSelected(ctx, LabelCluster+"="+key, h.c.ReapTimeout, func(members []v1alpha1.HostProcess) (bool, error) {
		pending = 0
		for _, o := range members {
			switch o.Status.Phase {
			case v1alpha1.HostProcessPhaseStopped, v1alpha1.HostProcessPhaseLost, v1alpha1.HostProcessPhaseFailed:
				if removed[o.Name] {
					continue
				}
				if err := h.remove(ctx, o.Name); err != nil {
					return false, err
				}
				removed[o.Name] = true
			default:
				pending++
			}
		}
		return pending == 0, nil
	})
	if timedOut(err) {
		return fmt.Errorf("clusterprovider: %d members of %s are still active after %s", pending, key, h.c.ReapTimeout)
	}
	if err != nil {
		return err
	}
	if p, err := h.controlPlanePath(key); err == nil {
		os.Remove(p)
	}
	h.c.Log.Info("cluster provider: every member of the cluster is stopped", "cluster", key, "slice", h.Slice(key))
	return nil
}

func (h *UnitsHost) LinkState(key string, service v1alpha1.ClusterLinkService) LinkRecord {
	cp, err := h.readControlPlane(key)
	if err != nil {
		return LinkRecord{Slot: ActiveSlot}
	}
	if r, ok := cp.Links[string(service)]; ok && r.Slot != "" {
		return r
	}
	return LinkRecord{Slot: ActiveSlot}
}