~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
188 lines · 6.2 KB Raw History
package hostrelease

import (
	"context"
	"errors"
	"fmt"
	"log/slog"
	"strings"
	"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 = "hostrelease"

	Export = "applications"

	ObjectName = "host-release"

	ConditionReady = "Ready"

	ConditionCurrent = "Current"

	ConditionSelfCheck = "SelfCheck"

	ConditionGC = "GC"

	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("hostrelease: 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("hostrelease: forwarding changes: %w", err)
		}
		options = append(options, ctlruntime.Events(events))
	}
	if err := ctlruntime.For[*v1alpha1.HostRelease](m, Name, c, options...); err != nil {
		return nil, err
	}
	return c, nil
}

func Desired(ctx context.Context, a hostlifecycle.Authority) (*v1alpha1.HostRelease, error) {
	obj := &v1alpha1.HostRelease{ObjectMeta: metav1.ObjectMeta{Name: ObjectName}}
	obs, err := a.ObserveHostRelease(ctx, v1alpha1.HostReleaseSpec{})
	obj.Spec.Version = obs.Current
	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.HostRelease) (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.ObserveHostRelease(ctx, spec)
	status := v1alpha1.HostReleaseStatus{ObservedGeneration: generation, Current: obs.Current, Previous: obs.Previous,
		SelfCheck: obs.SelfCheck, GC: obs.GC, Conditions: obj.Status.Conditions}
	if err != nil {
		c.log.Warn("reading the host release authority", "name", obj.Name, "err", err.Error())
		for _, kind := range []string{ConditionCurrent, ConditionSelfCheck, ConditionGC} {
			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)
	}
	matches := spec.Version == "" || obs.Current == spec.Version
	ready := obs.Current != "" && matches
	msg := message(obs, ready, matches)
	conds.SetStatus(&status.Conditions, generation, ConditionReady, conds.Status(ready), boolReason(ready, "Current", "NotCurrent"), msg)
	conds.SetStatus(&status.Conditions, generation, ConditionCurrent, conds.Status(ready), boolReason(ready, "MatchesSpec", "DiffersFromSpec"), msg)
	value, reason, selfMessage := selfCheckCondition(obs.SelfCheck)
	conds.SetStatus(&status.Conditions, generation, ConditionSelfCheck, value, reason, selfMessage)
	gcValue, gcReason, gcMessage := gcCondition(obs.GC)
	conds.SetStatus(&status.Conditions, generation, ConditionGC, gcValue, gcReason, gcMessage)
	return ctlruntime.Result{}, c.apply(ctx, req, obj, status)
}

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

func message(obs hostlifecycle.HostReleaseObservation, ready, matches bool) string {
	switch {
	case obs.Current == "":
		return "the current release is not known"
	case !matches:
		return fmt.Sprintf("the requested version is not current; the current version is %s", obs.Current)
	case ready:
		return fmt.Sprintf("version %s is current", obs.Current)
	default:
		return "the host release is not ready"
	}
}

func selfCheckCondition(sc *v1alpha1.HostReleaseSelfCheck) (metav1.ConditionStatus, string, string) {
	if sc == nil {
		return metav1.ConditionUnknown, "NoSelfCheck", "no self-check report has been recorded"
	}
	switch strings.ToLower(strings.TrimSpace(sc.Phase)) {
	case "ok", "pass", "passed", "ready", "succeeded", "success", "healthy":
		return metav1.ConditionTrue, "SelfCheckPassed", "the self-check passed"
	case "", "pending", "running", "queued":
		return metav1.ConditionUnknown, "SelfCheckPending", "the self-check has not finished"
	default:
		return metav1.ConditionFalse, "SelfCheckFailed", "the self-check reported phase " + sc.Phase
	}
}

func gcCondition(gc *v1alpha1.HostReleaseGC) (metav1.ConditionStatus, string, string) {
	if gc == nil {
		return metav1.ConditionUnknown, "NoGC", "no GC report has been recorded"
	}
	return metav1.ConditionTrue, "Collected", fmt.Sprintf("GC kept %d and removed %d", gc.Kept, len(gc.Removed))
}

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