~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
178 lines · 4.5 KB Raw History
package ttl

import (
	"context"
	"errors"
	"log/slog"
	"slices"
	"strings"
	"sync"
	"time"

	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/ttlctl"
)

const (
	Name = "ttl"

	ExportBid = "computevms"

	ExportComputeContract = "computecontracts"

	ReportKindsLimit = 40
)

type Options struct {
	Log *slog.Logger

	Scope func(workspace string) bool
}

type entry struct {
	kind string

	cluster string

	namespace string

	name string
}

type deletion struct {
	count int64

	last time.Time
}

type Controller struct {
	log *slog.Logger

	mu sync.Mutex

	tracked map[entry]struct{}

	deleted map[string]*deletion

	changedAt time.Time
}

func Register(m *ctlruntime.Manager, opts Options) (*Controller, error) {
	log := opts.Log
	if log == nil {
		log = slog.New(slog.DiscardHandler)
	}
	c := &Controller{log: log.With("component", "ttl-controller"), tracked: map[entry]struct{}{}, deleted: map[string]*deletion{},
		changedAt: time.Now()}
	err := errors.Join(
		register(m, c, opts, "Bid", ExportBid, func(o *v1alpha1.Bid) (*int32, []metav1.Condition) {
			return o.Spec.TTLSecondsAfterFinished, o.Status.Conditions
		}),
		register(m, c, opts, "ComputeContract", ExportComputeContract, func(o *v1alpha1.ComputeContract) (*int32, []metav1.Condition) {
			return o.Spec.TTLSecondsAfterFinished, o.Status.Conditions
		}))
	if err != nil {
		return nil, err
	}
	return c, nil
}

func register[T ctlruntime.Object](m *ctlruntime.Manager, c *Controller, opts Options, kind, export string, finish func(T) (*int32, []metav1.Condition)) error {
	r := &expiry[T]{c: c, kind: kind, finish: finish}
	options := []ctlruntime.Option{ctlruntime.Export(export),
		ctlruntime.Watches(func(_ context.Context, src ctlruntime.Request, obj T) []ctlruntime.Request {
			return []ctlruntime.Request{{Cluster: src.Cluster, Namespace: obj.GetNamespace(), Name: obj.GetName()}}
		}),
		ctlruntime.Gone(func(_ context.Context, req ctlruntime.Request) { c.forget(entryOf(kind, req)) })}
	if opts.Scope != nil {
		options = append(options, ctlruntime.InWorkspaces(opts.Scope))
	}
	return ctlruntime.For[T](m, Name+"-"+strings.ToLower(kind), r, options...)
}

func entryOf(kind string, req ctlruntime.Request) entry {
	return entry{kind: kind, cluster: req.Cluster, namespace: req.Namespace, name: req.Name}
}

type expiry[T ctlruntime.Object] struct {
	c *Controller

	kind string

	finish func(T) (*int32, []metav1.Condition)
}

func (r *expiry[T]) Reconcile(ctx context.Context, req ctlruntime.Request, obj T) (ctlruntime.Result, error) {
	key := entryOf(r.kind, req)
	if !obj.GetDeletionTimestamp().IsZero() {
		r.c.forget(key)
		return ctlruntime.Result{}, nil
	}
	ttl, conditions := r.finish(obj)
	at, due := ttlctl.Expiry(ttl, conditions)
	if !due {
		r.c.forget(key)
		return ctlruntime.Result{}, nil
	}
	if wait := time.Until(at); wait > 0 {
		r.c.track(key)
		return ctlruntime.Result{RequeueAfter: wait}, nil
	}
	if err := req.Delete(ctx, obj); err != nil {
		return ctlruntime.Result{}, err
	}
	r.c.removed(key)
	r.c.log.Info("an object finished more than its TTL ago is deleted", "workspace", req.Workspace, "kind", r.kind, "name", req.Name,
		"ttlSeconds", *ttl)
	return ctlruntime.Result{}, nil
}

func (c *Controller) track(key entry) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if _, ok := c.tracked[key]; !ok {
		c.tracked[key] = struct{}{}
		c.changedAt = time.Now()
	}
}

func (c *Controller) forget(key entry) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if _, ok := c.tracked[key]; ok {
		delete(c.tracked, key)
		c.changedAt = time.Now()
	}
}

func (c *Controller) removed(key entry) {
	c.mu.Lock()
	defer c.mu.Unlock()
	delete(c.tracked, key)
	d := c.deleted[key.kind]
	if d == nil {
		d = &deletion{}
		c.deleted[key.kind] = d
	}
	d.count++
	d.last = time.Now()
	c.changedAt = d.last
}

func (c *Controller) Report() *v1alpha1.TTLStatus {
	c.mu.Lock()
	defer c.mu.Unlock()
	st := &v1alpha1.TTLStatus{SampledAt: c.changedAt.UTC().Format(time.RFC3339), Tracked: int64(len(c.tracked))}
	for kind, d := range c.deleted {
		st.Deleted = append(st.Deleted, v1alpha1.TTLDeleted{Kind: kind, Count: d.count, LastAt: d.last.UTC().Format(time.RFC3339)})
		st.DeletedTotal += d.count
	}
	slices.SortFunc(st.Deleted, func(a, b v1alpha1.TTLDeleted) int { return strings.Compare(a.Kind, b.Kind) })
	if len(st.Deleted) > ReportKindsLimit {
		st.Deleted = st.Deleted[:ReportKindsLimit]
	}
	return st
}