~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
158 lines · 4.8 KB Raw History
package policytrigger

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

	policyv1alpha1 "github.com/johnandersen777/socialweb-computer-kcp/api/policy/v1alpha1"
	ctlruntime "github.com/johnandersen777/socialweb-computer-kcp/internal/controllers/runtime"
	pecontroller "github.com/johnandersen777/socialweb-computer-kcp/internal/policyengine/controller"
)

const (
	Name = "policytrigger"

	Export = "policy"
)

type ActionsFunc func(workspace string) (map[string]pecontroller.ActionHandler, error)

type Options struct {
	Actions ActionsFunc

	Log *slog.Logger
}

type Controller struct {
	actions ActionsFunc

	log *slog.Logger

	mu sync.Mutex

	handlers map[string]map[string]pecontroller.ActionHandler

	triggers map[string]map[string]struct{}

	runs map[string]map[string]*policyv1alpha1.PolicyWorkflowRun
}

func Register(m *ctlruntime.Manager, opts Options) (*Controller, error) {
	log := opts.Log
	if log == nil {
		log = slog.New(slog.DiscardHandler)
	}
	c := &Controller{actions: opts.Actions, log: log.With("component", "policytrigger-controller"),
		handlers: map[string]map[string]pecontroller.ActionHandler{}, triggers: map[string]map[string]struct{}{},
		runs: map[string]map[string]*policyv1alpha1.PolicyWorkflowRun{}}
	if err := ctlruntime.For[*policyv1alpha1.PolicyTrigger](m, Name, c, ctlruntime.Export(Export),
		ctlruntime.Watches[*policyv1alpha1.PolicyWorkflowRun](c.runChanged)); err != nil {
		return nil, fmt.Errorf("policytrigger: registering the PolicyTrigger controller: %w", err)
	}
	return c, nil
}

func (c *Controller) Reconcile(ctx context.Context, req ctlruntime.Request, trigger *policyv1alpha1.PolicyTrigger) (ctlruntime.Result, error) {
	c.track(req.Cluster, trigger.Name, trigger.DeletionTimestamp.IsZero())
	handlers, err := c.handlersFor(req)
	if err != nil {
		return ctlruntime.Result{}, err
	}
	reconciler := &pecontroller.TriggerReconciler{Handlers: handlers}
	state, err := reconciler.ReconcilePolicyTrigger(ctx, trigger)
	if err != nil {
		return ctlruntime.Result{}, err
	}
	state.LastRun = trigger.Status.LastRun
	state.NextRun = trigger.Status.NextRun
	if !pecontroller.TriggerStatusEqual(trigger.Status, state) {
		if err := c.apply(ctx, req, trigger, state); err != nil {
			return ctlruntime.Result{}, err
		}
	}
	runs := c.finished(req.Cluster)
	if len(runs) == 0 {
		return ctlruntime.Result{}, nil
	}
	dispatchErr := reconciler.DispatchRuns(ctx, trigger, runs, func(state pecontroller.TriggerReconcileState) error {
		return c.apply(ctx, req, trigger, state)
	})
	if dispatchErr != nil {
		c.log.Warn("dispatching a policy trigger", "trigger", trigger.Name, "workspace", req.Workspace, "err", dispatchErr.Error())
	}
	return ctlruntime.Result{}, dispatchErr
}

func (c *Controller) apply(ctx context.Context, req ctlruntime.Request, trigger *policyv1alpha1.PolicyTrigger, state pecontroller.TriggerReconcileState) error {
	object := trigger.DeepCopy()
	object.Status = policyv1alpha1.PolicyTriggerStatus{LastRun: state.LastRun, NextRun: state.NextRun, Active: state.Active, Conditions: state.Conditions}
	if err := req.ApplyStatus(ctx, object); err != nil {
		return err
	}
	trigger.Status = object.Status
	return nil
}

func (c *Controller) handlersFor(req ctlruntime.Request) (map[string]pecontroller.ActionHandler, error) {
	c.mu.Lock()
	handlers, ok := c.handlers[req.Cluster]
	c.mu.Unlock()
	if ok {
		return handlers, nil
	}
	if c.actions == nil {
		return nil, nil
	}
	handlers, err := c.actions(req.Workspace)
	if err != nil {
		return nil, errors.Join(errors.New("policytrigger: the trigger action handlers"), err)
	}
	c.mu.Lock()
	c.handlers[req.Cluster] = handlers
	c.mu.Unlock()
	return handlers, nil
}

func (c *Controller) track(cluster, name string, live bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if !live {
		delete(c.triggers[cluster], name)
		return
	}
	if c.triggers[cluster] == nil {
		c.triggers[cluster] = map[string]struct{}{}
	}
	c.triggers[cluster][name] = struct{}{}
}

func (c *Controller) finished(cluster string) []*policyv1alpha1.PolicyWorkflowRun {
	c.mu.Lock()
	defer c.mu.Unlock()
	out := make([]*policyv1alpha1.PolicyWorkflowRun, 0, len(c.runs[cluster]))
	for _, run := range c.runs[cluster] {
		out = append(out, run)
	}
	return out
}

func (c *Controller) runChanged(_ context.Context, src ctlruntime.Request, run *policyv1alpha1.PolicyWorkflowRun) []ctlruntime.Request {
	c.mu.Lock()
	defer c.mu.Unlock()
	if !run.DeletionTimestamp.IsZero() || !pecontroller.Finished(run) {
		delete(c.runs[src.Cluster], run.Name)
		return nil
	}
	if c.runs[src.Cluster] == nil {
		c.runs[src.Cluster] = map[string]*policyv1alpha1.PolicyWorkflowRun{}
	}
	c.runs[src.Cluster][run.Name] = run.DeepCopy()
	out := make([]ctlruntime.Request, 0, len(c.triggers[src.Cluster]))
	for name := range c.triggers[src.Cluster] {
		out = append(out, ctlruntime.Request{Cluster: src.Cluster, Name: name})
	}
	return out
}