~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
202 lines · 5.3 KB Raw History
package hostlifecycle

import (
	"cmp"
	"context"
	"errors"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/apiserverfront"
)

const (
	LinkCurrent = "current"

	LinkPrevious = "previous"
)

var ErrNoStateDir = errors.New("hostlifecycle: no host state directory is configured")

var ErrAccount = errors.New("hostlifecycle: the host account names no valid install record")

var ErrNoReader = errors.New("hostlifecycle: the host reader is not configured")

type KcpInstallationObservation struct {
	ActiveSlot   v1alpha1.KcpSlot
	FrontBackend string
	Drifted      bool
	DriftMessage string
	LeaseStale   bool
	LeaseMessage string
	LeaseStaleIn time.Duration
}

type HostInstallObservation struct {
	Found            bool
	Account          string
	InstalledVersion string
	Steps            []v1alpha1.HostInstallStep
}

type HostReleaseObservation struct {
	Current   string
	Previous  []string
	SelfCheck *v1alpha1.HostReleaseSelfCheck
	GC        *v1alpha1.HostReleaseGC
}

type Authority interface {
	ObserveKcpInstallation(ctx context.Context, spec v1alpha1.KcpInstallationSpec) (KcpInstallationObservation, error)
	ObserveHostInstall(ctx context.Context, spec v1alpha1.HostInstallSpec) (HostInstallObservation, error)
	ObserveHostRelease(ctx context.Context, spec v1alpha1.HostReleaseSpec) (HostReleaseObservation, error)
}

type InstallRecord struct {
	StateDir string
	Steps    map[string]string
}

type FileAuthorityOptions struct {
	StateDir        string
	Account         string
	FrontControl    string
	Record          func(account string) (InstallRecord, error)
	ActiveSlot      func(context.Context) (string, error)
	CoreVersion     func(ctx context.Context, stateDir, link string) (string, error)
	Drift           func(context.Context) (bool, error)
	Lease           func(context.Context) (time.Time, bool, error)
	LeaseStaleAfter time.Duration
	SelfCheck       func(context.Context, string) *v1alpha1.HostReleaseSelfCheck
	GC              func(context.Context) *v1alpha1.HostReleaseGC
	Now             func() time.Time
}

type FileAuthority struct {
	o FileAuthorityOptions
}

var _ Authority = (*FileAuthority)(nil)

func NewFileAuthority(o FileAuthorityOptions) *FileAuthority {
	return &FileAuthority{o: o}
}

func (a *FileAuthority) now() time.Time {
	if a.o.Now != nil {
		return a.o.Now()
	}
	return time.Now()
}

func (a *FileAuthority) ObserveKcpInstallation(ctx context.Context, _ v1alpha1.KcpInstallationSpec) (KcpInstallationObservation, error) {
	var out KcpInstallationObservation
	if a.o.ActiveSlot == nil {
		return out, ErrNoReader
	}
	active, err := a.o.ActiveSlot(ctx)
	if err != nil {
		return out, err
	}
	out.ActiveSlot = v1alpha1.KcpSlot(active)
	if a.o.FrontControl != "" {
		st, err := (apiserverfront.Client{Path: a.o.FrontControl}).Status(ctx)
		if err != nil {
			return out, err
		}
		out.FrontBackend = st.Backend
	}
	if a.o.Drift != nil {
		drifted, err := a.o.Drift(ctx)
		if err != nil {
			return out, err
		}
		if drifted {
			out.Drifted = true
			out.DriftMessage = "the active slot process differs from its unit"
		}
	}
	if a.o.Lease != nil && a.o.LeaseStaleAfter > 0 {
		renewed, found, err := a.o.Lease(ctx)
		if err != nil {
			return out, err
		}
		if found && !renewed.IsZero() {
			left := a.o.LeaseStaleAfter - a.now().Sub(renewed)
			if left < 0 {
				out.LeaseStale = true
				out.LeaseMessage = "the controller lease has not renewed for over " + a.o.LeaseStaleAfter.String()
			} else {
				out.LeaseStaleIn = left + time.Millisecond
			}
		}
	}
	return out, nil
}

func (a *FileAuthority) ObserveHostInstall(ctx context.Context, spec v1alpha1.HostInstallSpec) (HostInstallObservation, error) {
	var out HostInstallObservation
	account := cmp.Or(spec.Account, a.o.Account)
	if !validAccount(account) {
		return out, ErrAccount
	}
	out.Account = account
	if a.o.Record == nil {
		return out, ErrNoReader
	}
	rec, err := a.o.Record(account)
	if errors.Is(err, os.ErrNotExist) {
		return out, nil
	}
	if err != nil {
		return out, err
	}
	out.Found = true
	names := make([]string, 0, len(rec.Steps))
	for name := range rec.Steps {
		names = append(names, name)
	}
	sort.Strings(names)
	for _, name := range names {
		out.Steps = append(out.Steps, v1alpha1.HostInstallStep{Name: name, Phase: rec.Steps[name]})
	}
	stateDir := cmp.Or(rec.StateDir, a.o.StateDir)
	if stateDir != "" && a.o.CoreVersion != nil {
		if v, err := a.o.CoreVersion(ctx, stateDir, LinkCurrent); err == nil {
			out.InstalledVersion = v
		}
	}
	return out, nil
}

func (a *FileAuthority) ObserveHostRelease(ctx context.Context, _ v1alpha1.HostReleaseSpec) (HostReleaseObservation, error) {
	var out HostReleaseObservation
	if a.o.StateDir == "" {
		return out, ErrNoStateDir
	}
	if a.o.CoreVersion == nil {
		return out, ErrNoReader
	}
	cur, err := a.o.CoreVersion(ctx, a.o.StateDir, LinkCurrent)
	if err != nil {
		return out, err
	}
	out.Current = cur
	if prev, err := a.o.CoreVersion(ctx, a.o.StateDir, LinkPrevious); err == nil && prev != "" && prev != out.Current {
		out.Previous = []string{prev}
	}
	if a.o.SelfCheck != nil {
		out.SelfCheck = a.o.SelfCheck(ctx, out.Current)
	}
	if a.o.GC != nil {
		out.GC = a.o.GC(ctx)
	}
	return out, nil
}

func validAccount(account string) bool {
	return account != "" && account != "." && account != ".." && !strings.ContainsAny(account, `/\`)
}