~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
163 lines · 5.1 KB Raw History
package hostinstall

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"sync"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	ctlruntime "github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/hostlifecycle"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/conds"
)

const (
	Name = "hostinstall"

	Export = "applications"

	ObjectName = "host-install"

	ConditionReady = "Ready"

	ConditionInstalled = "Installed"

	ConditionProgressing = "Progressing"

	authorityUnreadable = "the host authority could not be read; the controller retries"
)

type Options struct {
	Log *slog.Logger

	Authority hostlifecycle.Authority

	Scope func(workspace string) bool

	Changed <-chan struct{}
}

type Controller struct {
	log *slog.Logger

	authority hostlifecycle.Authority

	mu sync.Mutex

	known map[ctlruntime.Request]struct{}
}

func Register(m *ctlruntime.Manager, opts Options) (*Controller, error) {
	if opts.Authority == nil {
		return nil, errors.New("hostinstall: an authority is required")
	}
	log := opts.Log
	if log == nil {
		log = slog.New(slog.DiscardHandler)
	}
	c := &Controller{log: log.With("component", Name+"-controller"), authority: opts.Authority, known: map[ctlruntime.Request]struct{}{}}
	options := []ctlruntime.Option{ctlruntime.Export(Export), ctlruntime.Gone(c.gone)}
	if opts.Scope != nil {
		options = append(options, ctlruntime.InWorkspaces(opts.Scope))
	}
	if opts.Changed != nil {
		events, err := ctlruntime.Forward(m, Name+": changes", opts.Changed, c.forwarded)
		if err != nil {
			return nil, fmt.Errorf("hostinstall: forwarding changes: %w", err)
		}
		options = append(options, ctlruntime.Events(events))
	}
	if err := ctlruntime.For[*v1alpha1.HostInstall](m, Name, c, options...); err != nil {
		return nil, err
	}
	return c, nil
}

func Desired(ctx context.Context, a hostlifecycle.Authority) (*v1alpha1.HostInstall, error) {
	obj := &v1alpha1.HostInstall{ObjectMeta: metav1.ObjectMeta{Name: ObjectName}}
	obs, err := a.ObserveHostInstall(ctx, v1alpha1.HostInstallSpec{})
	obj.Spec.Account, obj.Spec.Version = obs.Account, obs.InstalledVersion
	return obj, err
}

func (c *Controller) forwarded(struct{}) []ctlruntime.Request {
	return c.requests()
}

func (c *Controller) requests() []ctlruntime.Request {
	c.mu.Lock()
	defer c.mu.Unlock()
	out := make([]ctlruntime.Request, 0, len(c.known))
	for r := range c.known {
		out = append(out, r)
	}
	return out
}

func (c *Controller) gone(_ context.Context, req ctlruntime.Request) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.known, ctlruntime.Request{Cluster: req.Cluster, Name: req.Name})
}

func (c *Controller) Reconcile(ctx context.Context, req ctlruntime.Request, obj *v1alpha1.HostInstall) (ctlruntime.Result, error) {
	if !obj.DeletionTimestamp.IsZero() {
		c.gone(ctx, req)
		return ctlruntime.Result{}, nil
	}
	c.mu.Lock()
	c.known[ctlruntime.Request{Cluster: req.Cluster, Name: req.Name}] = struct{}{}
	c.mu.Unlock()
	spec := obj.Spec
	generation := obj.Generation
	obs, err := c.authority.ObserveHostInstall(ctx, spec)
	status := v1alpha1.HostInstallStatus{ObservedGeneration: generation, InstalledVersion: obs.InstalledVersion, Steps: obs.Steps,
		Conditions: obj.Status.Conditions}
	if err != nil {
		c.log.Warn("reading the host install authority", "name", obj.Name, "err", err.Error())
		for _, kind := range []string{ConditionInstalled, ConditionProgressing} {
			conds.SetStatus(&status.Conditions, generation, kind, metav1.ConditionUnknown, "AuthorityUnreadable", authorityUnreadable)
		}
		conds.SetStatus(&status.Conditions, generation, ConditionReady, metav1.ConditionFalse, "AuthorityUnreadable", authorityUnreadable)
		return ctlruntime.Result{}, errors.Join(c.apply(ctx, req, obj, status), err)
	}
	found := obs.Found
	matches := spec.Version == "" || obs.InstalledVersion == spec.Version
	ready := found && obs.InstalledVersion != "" && matches
	progressing := found && !ready
	msg := message(obs, ready, matches)
	conds.SetStatus(&status.Conditions, generation, ConditionInstalled, conds.Status(found), boolReason(found, "RecordFound", "NoRecord"), msg)
	conds.SetStatus(&status.Conditions, generation, ConditionReady, conds.Status(ready), boolReason(ready, "Installed", "NotInstalled"), msg)
	conds.SetStatus(&status.Conditions, generation, ConditionProgressing, conds.Status(progressing), boolReason(progressing, "AwaitingVersion", "Settled"), msg)
	return ctlruntime.Result{}, c.apply(ctx, req, obj, status)
}

func (c *Controller) apply(ctx context.Context, req ctlruntime.Request, obj *v1alpha1.HostInstall, status v1alpha1.HostInstallStatus) error {
	next := obj.DeepCopy()
	next.Status = status
	return req.ApplyStatus(ctx, next)
}

func message(obs hostlifecycle.HostInstallObservation, ready, matches bool) string {
	switch {
	case !obs.Found:
		return "no host install record"
	case !matches:
		return "the requested version is not installed"
	case !ready:
		return "the installed version is not known"
	default:
		return fmt.Sprintf("version %s is installed", obs.InstalledVersion)
	}
}

func boolReason(value bool, yes, no string) string {
	if value {
		return yes
	}
	return no
}